package engine.core;

import engine.loader.texture.TextureLoader;
import engine.scene.Scene;

import java.nio.FloatBuffer;
import java.util.ArrayList;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;

import static org.lwjgl.opengl.GL11.*;

/**
 * This abstract class provides a basic resizeable frame, containing one openGL
 * window. This window includes window decoration but does not support
 * fullscreen mode.
 * 
 * @author richard
 * 
 */
public abstract class OpenGLView implements Runnable {
	private ArrayList<Scene> scenes;
	private Scene currentScene;
	private Scene newCurrentScene;
	
	private int width;
	private int height;

	private String title;
	
	private boolean drag;
	
	private long timeElapsed;
	
	private int frames;
	private long timeToWait;
	
	private boolean isRunning;
	
	/**
	 * Setting up an openGL window with the specified title. 
	 * @param title
	 */
	public OpenGLView(String title) {
		this.title = title;
		scenes = new ArrayList<Scene>();
	}
	
	/**
	 * Setting up an openGL window with the specified title and size. 
	 * @param title
	 * @param width
	 * @param height
	 */
	public OpenGLView(String title, int width, int height) {
		this(title);
		this.width = width;
		this.height = height;
	}
	
	/**
	 * This method is the core function of the engine and is running the game loop. 
	 */
	@Override
	public void run() {
		//At first, create the openGL window. 
		createOpenGLWindow();
		
		//Initialize the standards, that are set all over the engine. 
		setStandards();
		
		//Call init, for initialize setup. 
		init();
		
		//Calling init on any added scene. 
		for(Scene s: scenes) {
			s.initAbs();
		}
		
		//Calling reshape once, that the screen can be resized. 
		reshape();
		
		//initialize timing variables for fps measurement
		timeElapsed = System.nanoTime();
		timeToWait = System.currentTimeMillis() + 1000;
		
		isRunning = true;
		
		//starting game loop. 
		while(isRunning && !Display.isCloseRequested()) {
			//this occurs, if a new scene is set. The change of scenes can only be applied in the game loop. 
			if(newCurrentScene != null) {
				if(currentScene != null) {
					currentScene.unsetCurrent();
				}
				currentScene = newCurrentScene;
				currentScene.setCurrent();
				newCurrentScene = null;
			}
			
			//Checks, if display size changed. If it change, the reshape method must be called and the scenes needs to be updated. 
			if(Display.wasResized()) {
				width = Display.getWidth();
				height = Display.getHeight();
				
				glViewport(0, 0, getWidth(), getHeight());
				
				for(Scene s: scenes) {
					s.updateView(width, height);
				}
				
				reshape();
			}
			
			//handles the input
			manageInput();
			
			//fps time measurement and rendering. 
			long lastTimeElapsed = timeElapsed;
			timeElapsed = System.nanoTime();
			float t = (System.nanoTime() - lastTimeElapsed) / 1000000f;
			render(t);
			
			//render the current scene
			if(currentScene != null) {
				currentScene.renderAbs(t);
			}
			
			frames++;
			
			if(timeToWait < System.currentTimeMillis()) {
				timeToWait = System.currentTimeMillis() + 1000;
				Display.setTitle(title + " - " + frames + " FPS");
				frames = 0;
			}
			
			//Swap the buffers and show the changes on the screen. 
			Display.update();
			reduceInputLag();
			
			//End game loop. 
		}
		
		//Clear any scene. 
		for(Scene s: scenes) {
			s.exit();
		}
		
		//Clear the program itself. 
		dealloc();
		TextureLoader.dealloc();
		Display.destroy();
	}
	
	private void setStandards() {
		FloatBuffer ambientLightModel = BufferUtils.createFloatBuffer(4).put(new float[]{0, 0, 0, 1});
		ambientLightModel.rewind();
		
		glEnable(GL_LIGHTING);
		glLightModel(GL_LIGHT_MODEL_AMBIENT, ambientLightModel);
	}

	/**
	 * Polling the events while waiting for vsync. 
	 */
	private void reduceInputLag() {
        glGetError();
        Display.processMessages();
        Mouse.poll();
        Keyboard.poll();
    }
	
	/**
	 * Handles the mouse and key input of the openGL window.  
	 */
	private void manageInput() {
		while (Keyboard.next()) {
			boolean keyState = Keyboard.getEventKeyState();
			int eventKey = Keyboard.getEventKey();
			
			if (keyState) {
				keyPressed(eventKey);
				if(currentScene != null) {
					currentScene.keyPressedAbs(eventKey);
				}
			} else {
				keyReleased(eventKey);
				if(currentScene != null) {
					currentScene.keyReleasedAbs(eventKey);
				}
			}
			 
		}
		
		while(Mouse.next()) {
			boolean buttonState = Mouse.getEventButtonState();
			int x = Mouse.getX();
			int y = Mouse.getY();
			int eventButton = Mouse.getEventButton();
			int wheel = Mouse.getEventDWheel();
			
			if(buttonState) {
				drag = true;
				mousePressed(x, y, eventButton);
				if(currentScene != null) {
					currentScene.mousePressedAbs(x, y, eventButton);
				}
			} else {
				if(Mouse.getEventButton() < 0) {
					if(drag) {
						mouseDragged(x, y, eventButton);
						if(currentScene != null) {
							currentScene.mouseDraggedAbs(x, y, eventButton);
						}
					} else {
						mouseMoved(x, y, eventButton);
						if(currentScene != null) {
							currentScene.mouseMovedAbs(x, y, eventButton);
						}
					}
				} else {
					drag = false;
					mouseReleased(x, y, eventButton);
					if(currentScene != null) {
						currentScene.mouseReleasedAbs(x, y, eventButton);
					}
				}
			}
			
			mouseWheelMoved(wheel / 120);
		}
	}
	
	/**
	 * Creates the openGL window, which is placed within the canvas. 
	 * 
	 * @param canvas
	 */
	private void createOpenGLWindow() {
		try {
			Display.setFullscreen(false);
			Display.setResizable(true);
			Display.setVSyncEnabled(true);
			Display.setTitle(title);
			Keyboard.enableRepeatEvents(true);
			
			if(width != 0 && height != 0) {
				Display.setDisplayMode(getDisplayMode(width, height));
			} else {
				Display.setDisplayMode(Display.getDesktopDisplayMode());
			}
			
			Display.create();
			
			width = Display.getWidth();
			height = Display.getHeight();
			
		} catch (LWJGLException e) {
			System.err.println("Error while creating the Display");
			System.exit(0);
		}
	}
	
	private DisplayMode getDisplayMode(int width, int height) throws LWJGLException {
		DisplayMode desktop = Display.getDesktopDisplayMode();
		ArrayList<DisplayMode> usableModes = new ArrayList<DisplayMode>();
		
		for(DisplayMode d: Display.getAvailableDisplayModes()) {
			if(d.getWidth() == width && d.getHeight() == height) {
				usableModes.add(d);
			}
		}
		
		if(usableModes.size() == 0) {
			return desktop;
		} else {
			DisplayMode best = usableModes.get(0);
			
			for(DisplayMode d: usableModes) {
				if(best.getBitsPerPixel() == desktop.getBitsPerPixel()) {
					if(Math.abs(desktop.getFrequency() - best.getFrequency()) > Math.abs(desktop.getFrequency() - d.getFrequency())) {
						best = d;
					}
				}
			}
			
			return best;
		}
	}
	
	public int getWidth() {
		return width;
	}
	
	public int getHeight() {
		return height;
	}
	
	public void setRunning(boolean isRunning) {
		this.isRunning = isRunning;
	}
	
	public boolean isRunning() {
		return isRunning;
	}
	
	/**
	 * Is called, directly after the openGL window is created. 
	 */
	public void init(){};
	
	/**
	 * Is called any time, the openGL window is resized. 
	 */
	public void reshape(){};
	
	/**
	 * Is called any time, when the openGL window renders. 
	 * @param timeElapsed
	 */
	public void render(float timeElapsed){};
	
	/**
	 * Is called, when the key is pressed. 
	 * @param key
	 */
	public void keyPressed(int key){};
	
	/**
	 * Is called, when the key is released. 
	 * @param key
	 */
	public void keyReleased(int key){};
	
	/**
	 * Is called, when the mouse is pressed. 
	 * @param posX
	 * @param posY
	 * @param button
	 */
	public void mousePressed(int posX, int posY, int button) {}
	
	/**
	 * Is called, when the mouse is released. 
	 * @param posX
	 * @param posY
	 * @param button
	 */
	public void mouseReleased(int posX, int posY, int button) {}
	
	/**
	 * Is called, when the mouse is moved. 
	 * @param posX
	 * @param posY
	 * @param button
	 */
	public void mouseMoved(int posX, int posY, int button) {}
	
	/**
	 * Is called, when the mouse is dragged. 
	 * @param posX
	 * @param posY
	 * @param button
	 */
	public void mouseDragged(int posX, int posY, int button) {};
	
	/**
	 * Is called, when the mouse wheel was moved. 
	 * @param movement
	 */
	public void mouseWheelMoved(int movement) {};
	
	/**
	 * Is called, directly before the openGL window is destroyed. 
	 */
	public void dealloc() {};
	
	public void addScene(Scene scene) {
		scene.setOpenGLView(this);
		scenes.add(scene);
	}
	
	public void removeScene(Scene scene) {
		scenes.remove(scene);
	}
	
	public Scene getCurrentScene() {
		return currentScene;
	}
	
	public void setCurrentScene(Scene currentScene) {
		newCurrentScene = currentScene;
	}
	
	public void setCurrentScene(String name) {
		for(Scene s: scenes) {
			if(s.getName() != null && s.getName().equals(name)) {
				newCurrentScene = s;
				return;
			}
		}
		
		throw new IllegalArgumentException("There is no scene with the name: " + name);
	}
}
